Skip to content

Fix/ns views not cleaned on removal #1740

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
May 10, 2019
Merged

Fix/ns views not cleaned on removal #1740

merged 22 commits into from
May 10, 2019

Conversation

m-abs
Copy link
Contributor

@m-abs m-abs commented Feb 18, 2019

PR Checklist

What is the current behavior?

See the description in #1738

What is the new behavior?

  • ViewUtil.removeChild(parent, child) now removes children recursively to clear references.
  • TemplatedItemsComponent.ngOnDestroy() empties the _templateMap to remove a circular reference to the TemplatedItemsComponent in the factory functions.
  • PageRouterOutlet.ngOnDestroy() will now destroy the activated componentRef.
  • NativeScriptRendererFactory.ngOnDestroy() will now destroy the children of the rootNgView.
  • NSLocationStrategy.ngOnDestroy() removes its outlets.

Fixes #1738

Related to #1728

@ghost ghost added the ♥ community PR label Feb 18, 2019
m-abs and others added 6 commits February 19, 2019 15:41
When removing the children from a Label and one of these children is
a FormattedString, the FormattedTextProperty throw this exception:
"TypeError: Cannot read property 'textTransform' of undefined"
Without this the Page and componentView doesn't get the proper relation
for then the page is destroyed.
…onent

PageRouterOutlet.loadComponentInPage moves the componentView from its native parent,
to the Page. Don't remove the componentView's children when it is removed from the
native parent.
@etabakov
Copy link
Contributor

@cla-bot check

@cla-bot cla-bot bot added the cla: yes label Feb 25, 2019
@cla-bot
Copy link

cla-bot bot commented Feb 25, 2019

The cla-bot has been summoned, and re-checked this pull request!

@edusperoni
Copy link
Collaborator

Upon reviewing #1767, I realized that view._modal is never dealt with.

This was usually not an issue, but when we navigate out (and this actually happens with livesync too), the modal will never close. Maybe the modal should also be destroyed on

private removeFromVisualTree(parent: NgView, child: NgView) {

Or maybe this should be handled by tns-core-modules on _removeView:

https://github.com/NativeScript/NativeScript/blob/3c2c1d9b69eac347474241af9303232043125d37/tns-core-modules/ui/core/view-base/view-base.ts#L614

@m-abs
Copy link
Contributor Author

m-abs commented Mar 22, 2019

I wanted to checkout the behavior, unfortunately I have trouble running ng-sample.

But I'm thinking ViewUtils.removeFromVisualTree should check for modal and close it.

@ghost ghost added the new PR label Apr 15, 2019
@m-abs
Copy link
Contributor Author

m-abs commented Apr 16, 2019

Hi @edusperoni,

Do you have an example where view.modal isn't dealt with?

I have been trying to replicate the problem without any success.

EDIT:
I can easily add code to handle it in ViewUtils.removeFromVisualTree, but I since I can't reproduce the problem I'm not sure it is won't cause problems.

@edusperoni
Copy link
Collaborator

I think this is mainly an issue when livesyncing. If you do:

Page 1 -> Page 2 -> open Modal

and livesync, it'll go back to page 1 and not close the modal. But I don't think it's something that should be considered in this PR

@ghost ghost assigned elena-p Apr 19, 2019
@ghost ghost added in progress and removed ♥ community PR labels Apr 19, 2019
@ghost ghost removed the in progress label Apr 24, 2019
@NativeScript NativeScript deleted a comment from SvetoslavTsenov May 3, 2019
@VladimirAmiorkov VladimirAmiorkov requested a review from vakrilov May 3, 2019 14:30

if (this.isActivated) {
const c = this.activated.instance;
this.activated.hostView.detach();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably no need to call detach on component that will be destroyed anyway

@vakrilov
Copy link
Contributor

vakrilov commented May 7, 2019

Hey there @m-abs
Looks like some really valid concerns are addressed in this PR.

The only change I didn't completely understand is ViewUtil.removeChild(parent, child) now removes children recursively to clear references. My thinking is that if you want to remove a large portion of the UI tree you should be able to do by just removing the root of the subtree. Why is there a need to removing each child recursively? There might be performance implication if there are many children or deeper UI tree. In the same time

@m-abs
Copy link
Contributor Author

m-abs commented May 7, 2019

@vakrilov

The only change I didn't completely understand is ViewUtil.removeChild(parent, child) now removes children recursively to clear references.

That is unfortunately needed to clear up all references to firstChild, lastChild and nextSibling from the UI tree below the view being removed.
These references make it hard for the GC to clean om the disposed Views even after the native views have been destroyed.

It might help if I give an example.

If we have a UI-tree like this:

View1
  View1_1
    View1_1_1
      View1_1_1_1
      View1_1_1_2
    View1_1.2
    View1_1_3
  View1_2
  View1_3
  View1_4

This gives us these references:

View1.firstChild = View1_1
View1.lastChild = View1_4
View1_1.nextSibling = View1_2
View1_2.nextSibling = View1_3
View1_3.nextSibling = View1_4
View1_1.firstChild = View1_1_1
View1_1.lastChild = View1_1.3
View1_1_1.firstChild = View1_1_1_1
View1_1_1.lastChild = View1_1_1_2
View1_1_1_1.nextSibling = View1_1_1_2
View1_1_1.nextSibling = View1_1_2
View1_1_3.nextSibling = View1_1_3

Removing View1_1 will give us these:

View1.firstChild = View1_1
View1.lastChild = View1_4
View1_2.nextSibling = View1_3
View1_3.nextSibling = View1_4

View1_1_1.firstChild = View1_1_1_1
View1_1_1.lastChild = View1_1_1_2
View1_1_1_1.nextSibling = View1_1_1_2

View1_1_1.nextSibling = View1_1_2
View1_1_3.nextSibling = View1_1_3

(To simplify the example I left out the parentNode/parent references created by tns-core-modules)

@edusperoni
Copy link
Collaborator

Shouldn't only the parent and siblings be updated then?

View1.firstChild = View1_2 // checked and updated
View1.lastChild = View1_4 // checked and remained the same
View1_2.nextSibling = View1_3 // checked and remained the same
View1_3.nextSibling = View1_4 // checked and remained the same

View1_1_1.firstChild = View1_1_1_1
View1_1_1.lastChild = View1_1_1_2
View1_1_1_1.nextSibling = View1_1_1_2

View1_1_1.nextSibling = View1_1_2
View1_1_2.nextSibling = View1_1_3 // this would be updated if View1_1_3 were removed

Since there is no more reference to View1_1_1, the whole tree should be GCed.

@edusperoni
Copy link
Collaborator

I've added a simple example to illustrate this behavior: https://stackblitz.com/edit/typescript-pbquxj

How to check garbage collection:

  1. Open Chrome on https://typescript-pbquxj.stackblitz.io
  2. open dev tools and go into "Performance" (DO NOT USE STACKBLITZ'S CONSOLE)
  3. Type in the console document.weakSet, all views will be there
  4. after 5s a view will disappear
  5. document.weakSet still shows all views
  6. CLEAR THE CONSOLE (this is important, as chrome will keep the reference to the elements inside the WeakSet)
  7. Press the Trash can (Collect garabage) in the performance tab a few times
  8. check that the views vanished from document.weakSet

The same exercise could be done in the NativeScript runtime with WeakRef, but I believe it'll behave the same.

@m-abs
Copy link
Contributor Author

m-abs commented May 8, 2019

Back when I started this PR, we had serious memory issues. It ended up delaying a release.

My stress test project as mentioned here could very easily crash. It would take about 1300 iterations or ~15 mins.

The behavior between [email protected] + [email protected] and [email protected] + [email protected] have improved a lot.

The stress test app no longer crash due to low memory, but it does end up using too much memory after many iterations.

I've been testing on Android using the Chrome Debugger to take heap snapshots manually. (I wish, I knew a better way to do it).

My reasoning behind removing the UI-tree recursively like that, was to make it easier for the Views to be GCed.

My guess is/was that are circular references in the sub-tree that makes it more difficult than needed for the GC.

Originally I just removed the firstChild, lastChild and nextSibling-references recursively in

private removeFromQueue(parent: NgView, child: NgView) {
, but I was afraid of causing some inconsistencies in the UI-tree.

I also tried making those properties into WeakRefs in

private setNgViewExtensions(view: View, name: string): NgView {
but it was too hacky for my taste.

@edusperoni
You're right with the latest tns-core-modules and tns-android.
The views are eventually GCed.

But GC takes longer, without recursively removing the child views.

I've run three stress tests with 1000 iterations.
There is a forced GC per 25 iteration.
Cold start each time.

1st test is with [email protected]:
Total time: 170442ms
Time spend on GC: 59443ms
Average GC time: 1486ms

2nd test is my PR without recursively removing children:
Total time: 171177ms
Time spend on GC: 59718ms
Average GC time: 1493ms

3rd test is with my PR as-is:
Total time: 165104ms
Time spend on GC: 52314ms
Average GC time: 1307ms

~7000ms less time spend on GC, if I remove the children recursively.

@vakrilov
Copy link
Contributor

vakrilov commented May 9, 2019

Indeed! The recursive remove does produce better results. I have also tried the same test with *ngFor instead of ListView and the results were consistently better.

Note: Adding "markingMode": "none" to the test project decreased the GC time from ~1600ms to ~65ms, which is x25 fold improvement. So I definitely recommend you to use it. In this setup the recursive update also performed slightly better.

That said - I'm sold on keeping the recursive remove. I have some other minor suggestions and we can merge this.

@m-abs
Copy link
Contributor Author

m-abs commented May 9, 2019

Note: Adding "markingMode": "none" to the test project decreased the GC time from ~1600ms to ~65ms, which is x25 fold improvement. So I definitely recommend you to use it. In this setup the recursive update also performed slightly better.

I tried "markingMode": "none" before and just now, it causes my test app to crash after a few iterations with:

05-09 10:29:01.224 19269 19269 W System.err: com.tns.NativeScriptException: Attempt to use cleared object reference id=9224
05-09 10:29:01.224 19269 19269 W System.err:    at com.tns.Runtime.getJavaObjectByID(Runtime.java:1006)
05-09 10:29:01.224 19269 19269 W System.err:    at com.tns.Runtime.callJSMethodNative(Native Method)
05-09 10:29:01.224 19269 19269 W System.err:    at com.tns.Runtime.dispatchCallJSMethodNative(Runtime.java:1203)
05-09 10:29:01.224 19269 19269 W System.err:    at com.tns.Runtime.callJSMethodImpl(Runtime.java:1083)
05-09 10:29:01.224 19269 19269 W System.err:    at com.tns.Runtime.callJSMethod(Runtime.java:1070)
05-09 10:29:01.224 19269 19269 W System.err:    at com.tns.Runtime.callJSMethod(Runtime.java:1050)
05-09 10:29:01.224 19269 19269 W System.err:    at com.tns.Runtime.callJSMethod(Runtime.java:1042)
05-09 10:29:01.224 19269 19269 W System.err:    at com.tns.gen.android.widget.BaseAdapter_view_197_32_ListViewAdapter.getView(BaseAdapter_view_197_32_ListViewAdapter.java:40)
...

But that belongs in another issue :)

That said - I'm sold on keeping the recursive remove. I have some other minor suggestions and we can merge this.

Cool :)

Copy link
Contributor

@vakrilov vakrilov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

@vakrilov
Copy link
Contributor

vakrilov commented May 9, 2019

Thanks @m-abs
We will try to include this in a patch release next week along with NativeScript 5.4 release

@SvetoslavTsenov
Copy link
Contributor

test ios@rc

@SvetoslavTsenov
Copy link
Contributor

test ios@rc

@ghost ghost assigned SvetoslavTsenov May 9, 2019
@ghost ghost added in progress and removed ♥ community PR labels May 9, 2019
@SvetoslavTsenov
Copy link
Contributor

test ios#rc

@vakrilov vakrilov merged commit 59a1cde into NativeScript:master May 10, 2019
@ghost ghost removed the in progress label May 10, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Potentiel leak: NativeScript views not cleaned up on removal
7 participants